Skip to content

feat(trace): opt-in lossless recording tier sharing the event pipeline - #2878

Merged
esokullu merged 8 commits into
webbrain-one:mainfrom
alectimison-maker:feat/trace-lossless-tier
Aug 22, 2026
Merged

feat(trace): opt-in lossless recording tier sharing the event pipeline#2878
esokullu merged 8 commits into
webbrain-one:mainfrom
alectimison-maker:feat/trace-lossless-tier

Conversation

@alectimison-maker

Copy link
Copy Markdown
Contributor

Summary

  • New opt-in lossless recording tier sharing the existing trace pipeline: when enabled, recordLLMRequest persists the full message/tool shape (clamped at 500 KB) instead of content-free provenance, and the tool-result cap rises from 20 KB to 200 KB. The default tier is byte-identical to today.
  • Tier wiring: startRun reads the losslessTrace storage key once per run (explicit meta.lossless override wins), stamps lossless on the run record, and restores the decision after SW eviction from the durable record (peekRunFlags).
  • Settings UI: "Record full request details (debug tier)" toggle, gated on tracing being enabled (turning tracing off clears the tier), with disclosure copy in all 23 locales.
  • Markdown export renders bounded, credential-masked message previews for lossless runs only; default-tier exports unchanged.

Motivation

Closes #2876.

WebBrain's privacy contract records traces without prompts/messages/tool schemas — the right default, but it leaves the single hardest debugging question unanswerable: "what exactly did the model see and say in this failing run?" The recorder had one tier; this adds a second, opt-in tier through the same pipeline, so deep debugging and request reconstruction are possible without weakening the default contract.

Design

  • Same event model, different write strategy: the tier is decided once per run in startRun (never per event), stored on the run record, and branched on inside recordLLMRequest / recordToolCall. SW-eviction recovery restores the tier from the durable record.
  • Bounds: request payloads clamp at 500 KB (head + {_truncated, length, head} marker, matching the existing tool-result convention); tool results keep up to 200 KB verbatim. Prevents an opt-in debugging tier from exhausting IndexedDB.
  • Privacy invariant stays structural: the default path is untouched (provenance summary, 20 KB cap) and pinned by tests; the tier is opt-in only and never forced for local runs.
  • Export: lossless llm_request events render up to 12 messages with a per-message preview cap and regex masking of credential shapes (sk-..., Bearer ..., api_key=/token=/password=); default runs render no messages. The masking helper is pure and browser-neutral, mirroring the strict-redaction spirit used elsewhere.
  • Settings: new boolean losslessTrace (default false) with the same storage pattern as tracingEnabled; the toggle disables (and clears) when tracing is off. Disclosure copy added to all 23 locales — the i18n en-fallback exists, but key parity is enforced by the locale tests, so keys are present everywhere.

Testing

  • node test/run.js — passed, 1973 tests (5 new: allowlist bounds, recorder tier wiring, settings UI wiring, masked export previews + default-tier no-preview, privacy guard)
  • npm run test:toolbar-guard — passed (33)
  • npm run test:security — passed (60/60)
  • node --check on every touched file — passed
  • Cross-check: merged with the sibling turn/step PR (both build on this baseline) in an integration branch — 1979 tests passed, no conflicts

One upstream pinning test was updated for the forced-flag recovery implementation change: peekRunFlags now restores forced + lossless from the durable run record instead of an extra isForcedTraceRun lookup; behavior is identical (the forced flag still comes from the run record).

Skipped: unpacked-browser manual verification (no browser session in this environment); the lossless tier's storage round-trip is covered by source-level and pure-module tests, and runtime IDB behavior in Chrome/Firefox is not manually exercised.

Compatibility and risks

  • Default tier: zero byte-level change (pinned by existing fixtures and the privacy tests).
  • New run-record field lossless is additive; old runs simply read as default tier.
  • The settings toggle lives on the Display tab; 23 locale files gain two keys each (label + disclosure), with per-language translations.
  • Risk: lossless runs can be large (bounded per request at 500 KB / per tool result at 200 KB); per-run storage budget/eviction is a deliberate follow-up.

Scope

Deferred (follow-ups): per-run storage budget/eviction, cloud-forced lossless runs, per-message opt-out, encryption at rest.

Adds a second, opt-in recording tier for the existing trace pipeline.
The default tier is byte-identical to today (content-free provenance,
20 KB tool-result cap); the lossless tier persists full LLM request
messages/tool schemas (clamped at 500 KB) and raises the tool-result
cap to 200 KB, enabling deep debugging and request reconstruction
without weakening the default privacy contract.

Tier wiring: startRun reads the opt-in 'losslessTrace' storage key
once per run (explicit meta.lossless override wins), stamps the run
record, and restores the decision after SW eviction from the durable
record via peekRunFlags. recordLLMRequest branches on the tier;
recordToolCall picks the cap from the run state.

Settings UI gains a 'Record full request details (debug tier)' toggle
gated on tracing being enabled, with disclosure copy in all 23
locales (en fallback never needed since keys are present everywhere).
The Markdown exporter renders bounded, credential-masked message
previews for lossless runs only; default-tier exports are unchanged.

One pinning test updated for the forced-flag recovery implementation
change (peekRunFlags replaces the in-memory lookahead; behavior
identical).

Mirrored to Firefox. Closes webbrain-one#2876
@vercel

vercel Bot commented Aug 21, 2026

Copy link
Copy Markdown

@alectimison-maker is attempting to deploy a commit to the esokullu's projects Team on Vercel.

A member of the Team first needs to authorize it.

@webbrain-one

Copy link
Copy Markdown
Owner

The lossless tier fails to remain lossless after service-worker recovery and its Markdown masking can expose common credential formats. Request bounds, truncated export handling, tier visibility, and the claimed default-format compatibility also remain incomplete.

Full review comments:

  • [P1] Restore lossless flags before branching — src/chrome/src/trace/recorder.js:252
    After a service-worker eviction, _runState is empty, so this check selects the default provenance-only path before _appendEvent can restore lossless via peekRunFlags. The first post-restart request therefore loses its messages/tools, and recordToolCall similarly uses the 20 KB cap; restore the durable flags before making tier-dependent decisions.

  • [P1] Redact quoted JSON credential fields — src/chrome/src/agent/trace-export.js:36
    For common message content such as {password:hunter2abc} or {api_key:value12345}, the quote after the key prevents this pattern from reaching :, so the credential is emitted verbatim in the Markdown export. Lossless exports may be shared under the expectation that credentials are masked; handle quoted keys or reuse the existing stricter credential redactor.

  • [P2] Bound tool schemas within the request cap — src/chrome/src/trace/recorder.js:265
    When a run has a large dynamic tool catalog, only messages is clamped while the complete tool schemas are copied here without a limit. Such a request can still be arbitrarily larger than the advertised 500 KB ceiling and exhaust IndexedDB; apply the cap to the combined request or independently clamp tools.

  • [P2] Render truncated lossless requests accurately — src/chrome/src/agent/trace-export.js:49-50
    Requests exceeding 500 KB are stored as a {_truncated, length, head} object, so this conversion produces an empty list and labels the request as (empty request log), also omitting its tools. Large vision or context-heavy requests will therefore have misleading Markdown exports; render the truncation marker/head instead of treating it as an empty request.

  • [P2] Surface lossless runs in the Traces UI — src/chrome/src/ui/settings.html:1508
    When users later inspect or export runs, the existing Traces UI never reads run.lossless, so full-payload traces containing prompts and credentials look identical to privacy-safe traces. Add the intended lossless badge or warning so users can identify sensitive historical runs before exporting them.

  • [P2] Preserve default-tier record bytes — src/chrome/src/trace/recorder.js:205
    With the toggle off, lossless is false but this property is still serialized into every run record, so default-tier traces are not byte-identical to the previous format as promised. Omit the field when false and treat absence as the default tier.

@webbrain-one

Copy link
Copy Markdown
Owner

Thanks for the follow-up. I re-reviewed the current head (54e5fa6fb9445820dcc3724277ba36dac1140687) after e85ba9194 and the merge from upstream/main.

What is fixed

The follow-up does address most of the original findings:

  • Lossless/forced flags are restored through _ensureRunState() before the tier branch after service-worker recovery.
  • The 500 KB request clamp now covers the combined messages + tool-schema payload instead of messages alone.
  • Truncated requests are rendered as truncated in Markdown and retain a bounded head plus tool names.
  • Lossless runs now have a badge and warning in the Traces UI.
  • Default-tier run records omit lossless when false, preserving the previous record shape.
  • The originally demonstrated quoted password / api_key JSON cases are now masked.
  • The branch includes the merged turn/step lifecycle work from feat(trace): turn/step boundary events with structured failure codes #2877.

What still blocks merge

  1. [P1] The Traces-page JSON export is still completely unmasked.

    src/chrome/src/ui/traces.js serializes the raw run events directly. In a lossless run, llm_request.messages and tools can contain passwords, API keys, tokens, and private request content. The new warning asks users to review the run before exporting, but the Traces timeline does not render those request fields, so there is no way to perform that review there. This also misses feat(trace): opt-in lossless recording tier sharing the event pipeline #2876's requirement that lossless exports apply credential masking. Please either apply the shared safe-export redactor before JSON serialization or make raw/unmasked export a separately explicit action with an appropriate confirmation. Firefox needs the same treatment.

  2. [P1] Markdown masking still covers only a subset of the project's credential catalog.

    The pattern in trace-export.js handles the original examples, but it does not cover credential keys already recognized elsewhere by WebBrain. I exercised the real tracesToMarkdown() exporter with sentinel values; refresh_token, client_secret, private_key, otp, and recovery_code all remained verbatim in the Markdown. Please reuse the complete credential-key/strict-redaction catalog rather than maintaining a narrower exporter-only list, and add these cases to the exporter test for both browsers.

  3. [P2] Tier restoration now occurs before the event enters the per-run write queue.

    recordLLMRequest() and recordToolCall() await _ensureRunState() before calling _appendEvent(), while many agent call sites intentionally fire-and-forget these helpers. After a service-worker restart, that IndexedDB restoration can still be pending when endRun() checks and flushes an empty _runWriteQueues entry. The request/tool event can then be appended after run finalization and be absent from final token/step/error accounting. This regresses the lifecycle durability guarantee added in feat(trace): turn/step boundary events with structured failure codes #2877. Please enqueue synchronously and perform state restoration/truncation inside the serialized write, or await every relevant caller and cover the restart + immediate-finalization case behaviorally.

  4. [P2] The per-run storage budget/eviction acceptance criterion from feat(trace): opt-in lossless recording tier sharing the event pipeline #2876 remains unimplemented.

    Per-request (500 KB) and per-result (200 KB) caps prevent one payload from being unbounded, but a long or unlimited-step lossless run can still accumulate an unbounded number of those payloads. feat(trace): opt-in lossless recording tier sharing the event pipeline #2876 explicitly scopes a per-run budget with oldest-runs-first eviction; the PR description currently defers it. Either implement that bound before closing feat(trace): opt-in lossless recording tier sharing the event pipeline #2876, or keep the issue open and link a concrete follow-up instead of treating the acceptance criteria as complete.

  5. The privacy documentation must be updated with the tiered contract.

    docs/privacy-and-data-flow.md still states unconditionally that trace request provenance never stores raw prompts, message text, tool schemas, or tool names. That is no longer true when the lossless tier is enabled.

Validation

The current head is syntactically clean and the existing suites are green:

  • node test/run.js: 1980/1980 passed
  • toolbar guard: 33/33 passed
  • security corpus: 60/60 passed
  • GitHub smoke: passed

Those suites currently do not exercise the raw JSON export, the broader credential-key set, or service-worker recovery followed by immediate finalization.

Recommendation: hold merge until the two export/privacy P1s and the finalization race are fixed. The storage-budget scope and documentation should also be resolved explicitly before this PR closes #2876.

@alectimison-maker

Copy link
Copy Markdown
Contributor Author

Follow-up pushed in 9bc5c04.

  • JSON exports now sanitize lossless payloads before serialization; Chrome reuses the cloud strict-redaction key catalog, and Firefox mirrors that catalog. Markdown now covers refresh tokens, client secrets, private keys, OTPs, and recovery codes.
  • Request/tool tier resolution now occurs inside the per-run serialized write, so endRun waits for recovery work already queued by fire-and-forget callers.
  • Lossless payloads now have a persisted 5 MB per-run budget; after it is reached, subsequent request/tool payloads are explicit budget-truncation markers.
  • Updated the privacy/data-flow documentation for the opt-in tier and masked exports.

Validated: node syntax checks and git diff --check. Full node test/run.js was started, but this environment did not complete it within the available command window; GitHub smoke is queued. Vercel remains an external authorization-required status.

@alectimison-maker

Copy link
Copy Markdown
Contributor Author

Follow-up pushed in 049daa8: lossless storage now has a 50 MB aggregate budget in addition to the 5 MB per-run cap. After each lossless request/tool write, completed lossless runs are sorted by startedAt and deleted oldest-first via deleteRun until the aggregate is within budget. The active/running run is never selected for eviction.

Also updated the recorder event-model assertion for the queue-internal deferred payload path. Syntax checks and git diff --check pass; GitHub checks will be refreshed for this head.

… tier pins

- add recovery to the sensitive-key code group in both catalogs
  (chrome cloud-runs.js, firefox trace-export.js) so credentials stored
  as structured args no longer survive sanitizeTraceExport
- extend the JSON export test with a structured-args case and a
  non-sensitive passthrough assertion
- update stale lossless-tier pins to the post-recovery write queue shape
  (losslessBytes stamp, queue-callback tier branch, tool cap)
@esokullu

Copy link
Copy Markdown
Collaborator

Reviewed 049daa822 and pushed fixes in 59f3c0d75.

Fixed

  • Red suite at head: three pins in trace lossless tier: recorder branches on the tier and clamps payloads still expected the pre-recovery shape (...(lossless ? { lossless: true } : {}), an await _ensureRunState(runId) prefix before the tier branch, an unanchored tool-cap regex). Updated to match the current write-queue shape.
  • [P1] recovery_code JSON leak: it was added to the Markdown masking but not to the key catalogs used by JSON export (SENSITIVE_CLOUD_KEY in cloud-runs.js, mirrored as SENSITIVE_TRACE_KEY in firefox trace-export.js). A payload like { name: 'fill_form', args: { recovery_code: '...' } } passed through sanitizeTraceExport verbatim because the value string gives maskSecrets nothing to match. Added recovery to the ...code group in both catalogs and extended the JSON export test with a structured-args case plus a non-sensitive passthrough assertion.

Suite is green at 59f3c0d75: 1982 passed, 0 failed.

Non-blocking, fine as follow-ups

  • Eviction reads only the newest 500 runs, so past that point older lossless runs are neither counted toward the 50 MB aggregate nor eligible for eviction; evictOldestLosslessRuns also does a full scan+sort on every lossless write even when far under cap.
  • Screenshot blobs aren't metered toward either budget.
  • Cosmetic: the budget-reached marker reports length: 0 where sibling truncation markers carry the true length.

- evictOldestLosslessRuns keeps a running aggregate so under-budget
  writes skip the store-wide scan entirely (one scan per worker
  lifetime to seed it, resynced on every eviction pass and clearAllRuns)
- the eviction scan walks every run instead of listRuns({ limit: 500 }),
  so older lossless runs count toward the 50 MB budget and stay
  evictable once past that window
- budget-reached truncation markers now carry the dropped payload's
  true length instead of length: 0
@esokullu

Copy link
Copy Markdown
Collaborator

Follow-up in 3e4ddccb9 for the non-blocking eviction items:

  • evictOldestLosslessRuns now keeps a cached running total (_losslessTotalEstimate), so under-budget writes return before any store-wide scan — the full pass only runs once per worker lifetime to seed the cache, when the estimate crosses the 50 MB cap, and after clearAllRuns. Every real pass resyncs the cache from the store, so drift from UI-side deletes just costs one extra scan rather than missed eviction.
  • The eviction scan walks all runs via a raw cursor instead of listRuns({ limit: 500 }), so lossless runs older than the newest-500 window are both counted toward the budget and evictable.
  • Budget-reached truncation markers (request and tool paths) now measure and carry the dropped payload's length instead of reporting length: 0.

Screenshot blobs remain unmetered — they live in a separate shots store and folding them into either budget needs its own decision, so I left it out.

Suite green at 3e4ddccb9: 1982 passed, 0 failed.

Keep both sides of the recorder conflict: the lossless tier state
loader/request clamp and the SW-eviction repair transaction added on
main touch disjoint functions.
@esokullu
esokullu merged commit ee8687a into webbrain-one:main Aug 22, 2026
1 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(trace): opt-in lossless recording tier sharing the event pipeline

3 participants